From d56fe8c38388706574aac4f0d7265127bddda8ff Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 3 Jun 2026 14:22:37 +0200 Subject: [PATCH 1/4] feat(configGuard): promote DEV_HOSTS/DEV_PORTS leak detection from trawl Adds DEV_HOSTS (localhost, 127.0.0.1) and DEV_PORTS (3000-8888 range) sets to assertConfigLoaded so any downstream that misconfigures api.host or api.port will get a hard build-time error instead of a silent prod outage. Guard is a no-op outside production mode. Adds 6 new unit tests covering localhost/127.0.0.1 host rejection, dev-port rejection (3010, 3000), and valid-config pass-through. Incident reference: trawl_vue #949 (signin-broken :3010 outage). Closes #4234. Partially closes pierreb-projects/infra#38 (T11). --- src/lib/helpers/configGuard.js | 42 ++++++++++++++++--- .../helpers/tests/configGuard.unit.tests.js | 38 +++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/lib/helpers/configGuard.js b/src/lib/helpers/configGuard.js index 430ab11f8..e34e37f04 100644 --- a/src/lib/helpers/configGuard.js +++ b/src/lib/helpers/configGuard.js @@ -6,24 +6,56 @@ const CONFIG_PLACEHOLDER = { port: 8080 }; /** - * Validate that the application config is fully loaded (not the placeholder). - * In production mode, a missing config is a fatal error because SEO plugins - * depend on real values. In development mode the placeholder is acceptable. + * Dev-only hostnames and ports that must never reach a production bundle. + * Root cause of 2026-05-10 signin-broken :3010 outage (issue #949). + */ +const DEV_HOSTS = new Set(['localhost', '127.0.0.1']); +const DEV_PORTS = new Set(['3000', '3001', '3010', '4000', '5000', '8000', '8080', '8888']); + +/** + * Validate that the application config is fully loaded (not the placeholder) + * and does not contain dev-default API coordinates in production builds. + * + * In production mode: + * - A missing/placeholder config is a fatal error (SEO plugins need real values). + * - api.host must not be localhost or 127.0.0.1. + * - api.port must not be a well-known local dev port. + * In development/test mode the function is a no-op. * * @param {object} config - The resolved application config object. * @param {string} mode - Vite build mode (e.g. 'production', 'development'). * @returns {void} - * @throws {Error} When mode is 'production' and config is still the placeholder. + * @throws {Error} When mode is 'production' and config is invalid or contains dev defaults. */ export const assertConfigLoaded = (config, mode) => { + if (mode !== 'production') return; + const safeConfig = config || CONFIG_PLACEHOLDER; const isPlaceholder = safeConfig === CONFIG_PLACEHOLDER || (Object.keys(safeConfig).length === 1 && safeConfig.port === 8080 && !safeConfig.host); - if (mode === 'production' && isPlaceholder) { + if (isPlaceholder) { throw new Error( 'Production build requires a valid config. Run `npm run config` to generate src/config/index.js before building.', ); } + + // Guard against dev-default API host leaking into a prod bundle (#949). + const apiHost = safeConfig?.api?.host; + if (apiHost && DEV_HOSTS.has(String(apiHost))) { + throw new Error( + `Production build has dev-default API host "${apiHost}". ` + + 'Set DEVKIT_VUE_api_host to the real API URL before building.', + ); + } + + // Guard against dev-default API port leaking into a prod bundle (#949). + const apiPort = safeConfig?.api?.port; + if (apiPort && DEV_PORTS.has(String(apiPort))) { + throw new Error( + `Production build has dev-default API port "${apiPort}". ` + + 'Set DEVKIT_VUE_api_port to "" (empty) or the real port before building.', + ); + } }; export { CONFIG_PLACEHOLDER }; diff --git a/src/lib/helpers/tests/configGuard.unit.tests.js b/src/lib/helpers/tests/configGuard.unit.tests.js index b4641984b..71b469bbe 100644 --- a/src/lib/helpers/tests/configGuard.unit.tests.js +++ b/src/lib/helpers/tests/configGuard.unit.tests.js @@ -40,4 +40,42 @@ describe('configGuard – assertConfigLoaded', () => { 'Production build requires a valid config', ); }); + + // DEV_HOSTS / DEV_PORTS leak-detection tests (incident ref: trawl_vue #949 — signin-broken :3010 outage) + + it('throws in production when api.host is localhost', () => { + expect(() => + assertConfigLoaded({ api: { host: 'localhost', port: '' } }, 'production'), + ).toThrow('dev-default API host'); + }); + + it('throws in production when api.host is 127.0.0.1', () => { + expect(() => + assertConfigLoaded({ api: { host: '127.0.0.1', port: '' } }, 'production'), + ).toThrow('dev-default API host'); + }); + + it('throws in production when api.port is a dev port', () => { + expect(() => + assertConfigLoaded({ api: { host: 'api.example.com', port: '3010' } }, 'production'), + ).toThrow('dev-default API port'); + }); + + it('throws in production when api.port is 3000', () => { + expect(() => + assertConfigLoaded({ api: { host: 'api.example.com', port: '3000' } }, 'production'), + ).toThrow('dev-default API port'); + }); + + it('does not throw in production with valid config (real host + empty port)', () => { + expect(() => + assertConfigLoaded({ api: { host: 'api.example.com', port: '' } }, 'production'), + ).not.toThrow(); + }); + + it('does not throw in production with valid config (real host + no port key)', () => { + expect(() => + assertConfigLoaded({ api: { host: 'api.example.com' } }, 'production'), + ).not.toThrow(); + }); }); From 83d31618dafa020e3b7cc63c4f38a99fb8d66180 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 3 Jun 2026 14:26:30 +0200 Subject: [PATCH 2/4] ci: set DEVKIT_VUE_api_host on build step so configGuard passes in devkit CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devkit Vue doesn't deploy, so no real API host is set. The new configGuard rejects a localhost default at production build time — add a placeholder value so the guard is satisfied without weakening the downstream check. --- .github/workflows/CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4b684f605..badcd83ee 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -37,6 +37,8 @@ jobs: - run: npm run lint - run: npm run test:unit:coverage - run: UV_USE_IO_URING=0 npm run build + env: + DEVKIT_VUE_api_host: https://api.example.com - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} From 2ed612616b26a02ae01249849e16fa8f39824c28 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 3 Jun 2026 14:29:33 +0200 Subject: [PATCH 3/4] ci: also clear DEVKIT_VUE_api_port for configGuard build configGuard rejects the dev-default port "3000" too. Set it to empty so the production-build guard passes in devkit CI (no real deploy target). --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index badcd83ee..4c706c901 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -39,6 +39,7 @@ jobs: - run: UV_USE_IO_URING=0 npm run build env: DEVKIT_VUE_api_host: https://api.example.com + DEVKIT_VUE_api_port: "" - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} From 06bd9f771ce76446c40ece86da303e629300c86b Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 3 Jun 2026 14:34:20 +0200 Subject: [PATCH 4/4] fix(configGuard): normalize host/port before DEV set membership check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Trim + lowercase api.host before DEV_HOSTS.has() — "LOCALHOST" no longer bypasses the guard - Trim api.port string before DEV_PORTS.has() — " 3000 " no longer bypasses - Fix error message: npm run config → npm run generateConfig (matches package.json) - Fix issue ref: #949 → trawl_vue#949 (cross-repo clarity) - Fix error copy: "real API URL" → "host/hostname" (api.host is hostname, not full URL) - Add regression tests for LOCALHOST (uppercase) and " 3000 " (padded) bypass attempts --- src/lib/helpers/configGuard.js | 20 +++++++++---------- .../helpers/tests/configGuard.unit.tests.js | 14 +++++++++++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/lib/helpers/configGuard.js b/src/lib/helpers/configGuard.js index e34e37f04..b7577ab47 100644 --- a/src/lib/helpers/configGuard.js +++ b/src/lib/helpers/configGuard.js @@ -7,7 +7,7 @@ const CONFIG_PLACEHOLDER = { port: 8080 }; /** * Dev-only hostnames and ports that must never reach a production bundle. - * Root cause of 2026-05-10 signin-broken :3010 outage (issue #949). + * Root cause of 2026-05-10 signin-broken :3010 outage (trawl_vue#949). */ const DEV_HOSTS = new Set(['localhost', '127.0.0.1']); const DEV_PORTS = new Set(['3000', '3001', '3010', '4000', '5000', '8000', '8080', '8888']); @@ -35,22 +35,22 @@ export const assertConfigLoaded = (config, mode) => { safeConfig === CONFIG_PLACEHOLDER || (Object.keys(safeConfig).length === 1 && safeConfig.port === 8080 && !safeConfig.host); if (isPlaceholder) { throw new Error( - 'Production build requires a valid config. Run `npm run config` to generate src/config/index.js before building.', + 'Production build requires a valid config. Run `npm run generateConfig` to generate src/config/index.js before building.', ); } - // Guard against dev-default API host leaking into a prod bundle (#949). - const apiHost = safeConfig?.api?.host; - if (apiHost && DEV_HOSTS.has(String(apiHost))) { + // Guard against dev-default API host leaking into a prod bundle (trawl_vue#949). + const apiHost = String(safeConfig?.api?.host || '').trim().toLowerCase(); + if (apiHost && DEV_HOSTS.has(apiHost)) { throw new Error( - `Production build has dev-default API host "${apiHost}". ` + - 'Set DEVKIT_VUE_api_host to the real API URL before building.', + `Production build has dev-default API host/hostname "${safeConfig?.api?.host}". ` + + 'Set DEVKIT_VUE_api_host to the real host/hostname before building.', ); } - // Guard against dev-default API port leaking into a prod bundle (#949). - const apiPort = safeConfig?.api?.port; - if (apiPort && DEV_PORTS.has(String(apiPort))) { + // Guard against dev-default API port leaking into a prod bundle (trawl_vue#949). + const apiPort = String(safeConfig?.api?.port || '').trim(); + if (apiPort && DEV_PORTS.has(apiPort)) { throw new Error( `Production build has dev-default API port "${apiPort}". ` + 'Set DEVKIT_VUE_api_port to "" (empty) or the real port before building.', diff --git a/src/lib/helpers/tests/configGuard.unit.tests.js b/src/lib/helpers/tests/configGuard.unit.tests.js index 71b469bbe..c3f5f65c1 100644 --- a/src/lib/helpers/tests/configGuard.unit.tests.js +++ b/src/lib/helpers/tests/configGuard.unit.tests.js @@ -78,4 +78,18 @@ describe('configGuard – assertConfigLoaded', () => { assertConfigLoaded({ api: { host: 'api.example.com' } }, 'production'), ).not.toThrow(); }); + + // Normalization regression tests: case and whitespace must not bypass guards. + + it('throws in production when api.host is "LOCALHOST" (uppercase bypass attempt)', () => { + expect(() => + assertConfigLoaded({ api: { host: 'LOCALHOST', port: '' } }, 'production'), + ).toThrow('dev-default API host'); + }); + + it('throws in production when api.port is " 3000 " (padded bypass attempt)', () => { + expect(() => + assertConfigLoaded({ api: { host: 'api.example.com', port: ' 3000 ' } }, 'production'), + ).toThrow('dev-default API port'); + }); });