diff --git a/cli/STYLE_GUIDE.md b/cli/STYLE_GUIDE.md index 615e2bef3a..a813ff0d36 100644 --- a/cli/STYLE_GUIDE.md +++ b/cli/STYLE_GUIDE.md @@ -283,8 +283,8 @@ tasks.start(deployIdx); ### Screen Layout ``` - ✓ Server ready at http://veryfront.me:3000 - ✓ MCP ready at http://veryfront.me:3001/mcp + ✓ Server ready at http://localhost:3000 + ✓ MCP ready at http://localhost:3001/mcp Projects ❯ my-agent diff --git a/cli/app/actions.test.ts b/cli/app/actions.test.ts index 3e849c437b..15ba6038c8 100644 --- a/cli/app/actions.test.ts +++ b/cli/app/actions.test.ts @@ -60,7 +60,7 @@ describe("app/actions", () => { const host = fakeHost(); const result = await createLauncher(host).openInBrowser(LOCAL, 8080); - assertEquals(host.urls, ["http://alpha.veryfront.me:8080"]); + assertEquals(host.urls, ["http://alpha.localhost:8080"]); assertEquals(result.success, true); }); diff --git a/cli/app/actions.ts b/cli/app/actions.ts index 9cdfb8f470..763da2fd7c 100644 --- a/cli/app/actions.ts +++ b/cli/app/actions.ts @@ -104,7 +104,7 @@ export function createLauncher(host: LauncherHost): Launcher { return { openInBrowser(project, port) { return open( - `http://${project.slug}.veryfront.me:${port}`, + `http://${project.slug}.localhost:${port}`, (url) => `Opened ${url}`, ); }, diff --git a/cli/app/shell.ts b/cli/app/shell.ts index e0363b198b..c3f80acdd2 100644 --- a/cli/app/shell.ts +++ b/cli/app/shell.ts @@ -84,7 +84,7 @@ export function createApp(config: AppConfig): App { state = updateServer({ port: config.port, - url: `http://veryfront.me:${config.port}`, + url: `http://localhost:${config.port}`, })(state); state = updateMCP({ @@ -368,8 +368,8 @@ export function createApp(config: AppConfig): App { running = true; if (!isInteractiveMode) { - console.log(`Server running on http://veryfront.me:${config.port}`); - if (config.mcpPort) console.log(`MCP available at http://veryfront.me:${config.mcpPort}/mcp`); + console.log(`Server running on http://localhost:${config.port}`); + if (config.mcpPort) console.log(`MCP available at http://localhost:${config.mcpPort}/mcp`); return; } diff --git a/cli/app/state.ts b/cli/app/state.ts index 7aad6c73b6..30eaaad244 100644 --- a/cli/app/state.ts +++ b/cli/app/state.ts @@ -108,7 +108,7 @@ export function createInitialState(): AppState { previousView: null, server: { running: false, - url: "http://veryfront.me:8080", + url: "http://localhost:8080", port: 8080, errors: 0, warnings: 0, diff --git a/cli/app/views/dashboard.ts b/cli/app/views/dashboard.ts index 4a035df5bd..3458ad4101 100644 --- a/cli/app/views/dashboard.ts +++ b/cli/app/views/dashboard.ts @@ -68,7 +68,7 @@ function renderStatus(state: AppState): string { const lines = [` ✓ Server ready at ${brand(state.server.url)}`]; if (state.mcp.enabled && state.mcp.transport === "http" && state.mcp.httpPort !== undefined) { - lines.push(` ✓ MCP ready at ${brand(`http://veryfront.me:${state.mcp.httpPort}/mcp`)}`); + lines.push(` ✓ MCP ready at ${brand(`http://localhost:${state.mcp.httpPort}/mcp`)}`); } const { errors, warnings } = state.server; diff --git a/cli/app/views/help.ts b/cli/app/views/help.ts index 5e939aabb5..7bab33d3a1 100644 --- a/cli/app/views/help.ts +++ b/cli/app/views/help.ts @@ -48,7 +48,7 @@ export function renderHelpView(state: AppState): string { lines.push(` ${dim('"mcpServers": {')}`); lines.push(` ${dim(' "veryfront": {')}`); lines.push(` ${dim(' "type": "url",')}`); - lines.push(` ${dim(` "url": "http://veryfront.me:${state.mcp.httpPort}/mcp"`)}`); + lines.push(` ${dim(` "url": "http://localhost:${state.mcp.httpPort}/mcp"`)}`); lines.push(` ${dim(" }")}`); lines.push(` ${dim("}")}`); lines.push(""); diff --git a/cli/app/views/presentation.test.ts b/cli/app/views/presentation.test.ts index f1466ddb97..2ab7adfc49 100644 --- a/cli/app/views/presentation.test.ts +++ b/cli/app/views/presentation.test.ts @@ -21,11 +21,11 @@ describe("CLI app presentation", () => { it("renders dashboard status without a decorative box or mascot", () => { const state = updateServer({ port: 3000, - url: "http://veryfront.me:3000", + url: "http://localhost:3000", })(createInitialState()); const output = renderEmptyState(state); - assertStringIncludes(output, "http://veryfront.me:3000"); + assertStringIncludes(output, "http://localhost:3000"); assertEquals(output.includes("╭"), false); assertEquals(output.includes("╰"), false); }); diff --git a/cli/commands/dev/command.ts b/cli/commands/dev/command.ts index 6c93615850..2d00cebd48 100644 --- a/cli/commands/dev/command.ts +++ b/cli/commands/dev/command.ts @@ -338,14 +338,14 @@ export function devCommand(options: DevOptions): Promise { }; } - const serverUrl = `http://veryfront.me:${boundPort}`; + const serverUrl = `http://localhost:${boundPort}`; const elapsed = Date.now() - startTime; console.log(); console.log(` ✓ Ready in ${formatDuration(elapsed)}`); console.log(` ${brand(serverUrl)}`); if (mcpServer && isVerbose()) { - console.log(` ${dim("MCP")} ${brand(`http://veryfront.me:${mcpPort}/mcp`)}`); + console.log(` ${dim("MCP")} ${brand(`http://localhost:${mcpPort}/mcp`)}`); } if (isTTY()) { console.log(devShortcuts()); diff --git a/cli/commands/mcp/command-help.ts b/cli/commands/mcp/command-help.ts index 8be2290fb7..5466ece55e 100644 --- a/cli/commands/mcp/command-help.ts +++ b/cli/commands/mcp/command-help.ts @@ -19,7 +19,7 @@ export const mcpHelp: CommandHelp = { "The CLI MCP server is development-only. Production start does not expose vf_* tools.", "", "Claude Code setup (~/.claude.json):", - ` "mcpServers": { "veryfront": { "url": "http://veryfront.me:${DEFAULT_DEV_MCP_PORT}/mcp" } }`, + ` "mcpServers": { "veryfront": { "url": "http://localhost:${DEFAULT_DEV_MCP_PORT}/mcp" } }`, "", "Available tools:", " • vf_list_local_projects - Discover projects on filesystem", diff --git a/cli/mcp/server.ts b/cli/mcp/server.ts index 79004bdc71..edea3c33e0 100644 --- a/cli/mcp/server.ts +++ b/cli/mcp/server.ts @@ -34,11 +34,12 @@ import { ToolsCallParamsSchema, } from "./jsonrpc.ts"; +// Exact loopback origins only. `localhost` is the hostname the CLI prints. +// Project subdomains were never admitted here and still are not. const ALLOWED_HTTP_ORIGIN_HOSTS = new Set([ "localhost", "127.0.0.1", "[::1]", - "veryfront.me", ]); function isAllowedHttpOrigin(origin: string): boolean { diff --git a/cli/mcp/tools.ts b/cli/mcp/tools.ts index 9ac7c8f8d2..380e059bbc 100644 --- a/cli/mcp/tools.ts +++ b/cli/mcp/tools.ts @@ -191,7 +191,7 @@ export function createVfGetStatus( return { running: true, - url: `http://veryfront.me:${port}`, + url: `http://localhost:${port}`, port, errorCount: counts.compile + counts.runtime + counts.bundle, warningCount: logs.query({ level: "warn" }).length, diff --git a/cli/mcp/tools/dev-tools.ts b/cli/mcp/tools/dev-tools.ts index 1a43af2891..ccee3e5da5 100644 --- a/cli/mcp/tools/dev-tools.ts +++ b/cli/mcp/tools/dev-tools.ts @@ -109,7 +109,7 @@ export const vfGetDebugContext: MCPTool { - const host = input.project ? `${input.project}.veryfront.me` : "veryfront.me"; + const host = input.project ? `${input.project}.localhost` : "localhost"; const url = `http://${host}:${input.port}/_vf_debug/context`; try { diff --git a/docs/api-reference/veryfront/agent.md b/docs/api-reference/veryfront/agent.md index ce56c8f259..8e099361db 100644 --- a/docs/api-reference/veryfront/agent.md +++ b/docs/api-reference/veryfront/agent.md @@ -886,7 +886,7 @@ Input delivered to a hosted agent-service detached execution callback. | `normalizeParsedHostedChatRequest` | Request payload for normalize parsed hosted chat. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-preparation.ts#L292) | | `normalizeRuntimeSkillReferencePath` | Normalizes and bounds a portable runtime skill reference path. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/skill-metadata.ts#L1010) | | `parseAgentServiceChatRequestFromRequest` | Request payload for parse hosted chat request from. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-request-parser.ts#L439) | -| `parseAgentServiceConfig` | Configuration used by parse agent service. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L157) | +| `parseAgentServiceConfig` | Configuration used by parse agent service. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L159) | | `parseAgUiContextBoolean` | Parses AG-UI context boolean. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/forwarded-context.ts#L56) | | `parseAgUiContextJsonValue` | Parses AG-UI context JSON value. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/forwarded-context.ts#L27) | | `parseAgUiContextNullableString` | Parses AG-UI context nullable string. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/forwarded-context.ts#L46) | @@ -899,7 +899,7 @@ Input delivered to a hosted agent-service detached execution callback. | `parseAgUiSseResponse` | Parse an AG-UI SSE `Response` into normalized events, text, tool starts, and terminal error state. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/sse-parser.ts#L258) | | `parseAppendConversationRunEventsErrorBody` | Parses append conversation run events error body. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/durable-append-errors.ts#L22) | | `parseDataStreamSseEvents` | Parses data stream sse events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/data-stream.ts#L14) | -| `parseHostedAgentServiceConfig` | Configuration used by parse hosted agent service. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L165) | +| `parseHostedAgentServiceConfig` | Configuration used by parse hosted agent service. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L167) | | `parseHostedChatRequestFromRequest` | Request payload for parse hosted chat request from. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-request-parser.ts#L439) | | `parseRuntimeAgentMarkdownDefinition` | Definition for parse runtime agent markdown. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/agent-definition.ts#L172) | | `parseRuntimeAgentRunInvocation` | Parses runtime agent run invocation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/agent-invocation-contract.ts#L461) | @@ -1438,8 +1438,8 @@ Input delivered to a hosted agent-service detached execution callback. | `HostedAgentRunSpanFinalState` | State for hosted agent run span final. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/agent-run-lifecycle.ts#L28) | | `HostedAgentRunTracer` | Public API contract for hosted agent run tracer. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/agent-run-lifecycle.ts#L23) | | `HostedAgentServiceActiveSpanAttributes` | Public API contract for hosted agent service active span attributes. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/routes.ts#L52) | -| `HostedAgentServiceConfig` | Configuration used by hosted agent service. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L152) | -| `HostedAgentServiceConfigInput` | Input payload for hosted agent service config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L154) | +| `HostedAgentServiceConfig` | Configuration used by hosted agent service. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L154) | +| `HostedAgentServiceConfigInput` | Input payload for hosted agent service config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L156) | | `HostedAgentServiceDetachedCleanupInput` | Input payload for hosted agent service detached cleanup. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/routes.ts#L83) | | `HostedAgentServiceDetachedExecutionInput` | Input delivered to a hosted agent-service detached execution callback. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/routes.ts#L71) | | `HostedAgentServiceEnvFileLoadOptions` | Options accepted by hosted agent service env file load. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/env-files.ts#L21) | @@ -1844,7 +1844,7 @@ Input delivered to a hosted agent-service detached execution callback. | Name | Description | Source | | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `agentServiceAgUiChatForwardedConfigSchema` | Schema for agent service AG-UI chat forwarded config. Schema for hosted AG-UI chat forwarded config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/ag-ui-chat-request.ts#L39) | -| `agentServiceConfigSchema` | Zod schema for agent service config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L147) | +| `agentServiceConfigSchema` | Zod schema for agent service config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L149) | | `agentServiceRegistrationConfigSchema` | Zod schema for agent service registration config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/registration.ts#L85) | | `agUiSseEventTypes` | AG-UI runtime event type constants normalized from browser-wire SSE events. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/ag-ui/sse-parser.ts#L5) | | `conversationRunEventTypes` | Shared conversation run event types value. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/conversation/run-events.ts#L7) | @@ -1877,7 +1877,7 @@ Input delivered to a hosted agent-service detached execution callback. | `getRuntimeClientProfileSchema` | Zod schema for get runtime client profile. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/client-profile.ts#L23) | | `getRuntimeClientTypeSchema` | Zod schema for get runtime client type. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/runtime/client-profile.ts#L5) | | `hostedAgentProjectSteeringOptionsSchema` | Zod schema for hosted agent project steering options. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/agent-project-steering.ts#L35) | -| `hostedAgentServiceConfigSchema` | Zod schema for hosted agent service config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L150) | +| `hostedAgentServiceConfigSchema` | Zod schema for hosted agent service config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/service/config.ts#L152) | | `hostedAgUiChatForwardedConfigSchema` | Schema for agent service AG-UI chat forwarded config. Schema for hosted AG-UI chat forwarded config. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/ag-ui-chat-request.ts#L39) | | `hostedChatRequestSchema` | Schema for hosted chat request. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-request.ts#L465) | | `hostedChatRuntimeOverridesSchema` | Schema for hosted chat runtime overrides. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/hosted/chat-request.ts#L47) | diff --git a/docs/api-reference/veryfront/server.md b/docs/api-reference/veryfront/server.md index 4b55b48b7c..744973252d 100644 --- a/docs/api-reference/veryfront/server.md +++ b/docs/api-reference/veryfront/server.md @@ -49,7 +49,7 @@ await server.fetch(new Request("https://example.com/health")); | `createHandler` | Create a Veryfront request handler for development or production. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/index.ts#L233) | | `createVeryfrontServer` | Create veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L157) | | `gracefullyShutdownProductionServer` | Enter lame-duck mode, mark readiness false, drain tracked requests and SSE response bodies, and stop a production server process. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/graceful-shutdown.ts#L218) | -| `parseProjectDomain` | Extract project slug and branch from domain/host header | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L70) | +| `parseProjectDomain` | Extract project slug and branch from domain/host header | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/utils/domain-parser.ts#L73) | | `startDevServer` | Starts dev server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/dev-server/index.ts#L15) | | `startNodeVeryfrontServer` | Starts node veryfront server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/service-server.ts#L575) | | `startProductionServer` | Starts production server. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/server/production-server.ts#L180) | diff --git a/docs/getting-started/create-project.md b/docs/getting-started/create-project.md index 73cfe94f44..71f48ab8c5 100644 --- a/docs/getting-started/create-project.md +++ b/docs/getting-started/create-project.md @@ -105,12 +105,12 @@ The CLI prints the URL it is serving on: ``` ✓ Ready in 1.3s - http://veryfront.me:3000 + http://localhost:3000 ``` -Open [http://veryfront.me:3000](http://veryfront.me:3000). `veryfront.me` -resolves to `127.0.0.1`, so [http://localhost:3000](http://localhost:3000) -reaches the same server. File changes reload the browser. +Open [http://localhost:3000](http://localhost:3000). `localhost` resolves to +`127.0.0.1` on every machine without a DNS lookup. File changes reload the +browser. ### Change the port @@ -127,7 +127,7 @@ scans forward for the first free port, reports the switch, and serves there: ! Port 3000 is in use, using 3001 instead ✓ Ready in 925ms - http://veryfront.me:3001 + http://localhost:3001 ``` Open the URL the CLI prints, not the one in the examples above. The development @@ -205,5 +205,5 @@ Claude Code, Cursor, Codex, or another MCP-aware agent. ## Verify it worked `veryfront dev` prints a `Ready in ` line followed by -`http://veryfront.me:3000`. Open that URL and save a source file. The browser +`http://localhost:3000`. Open that URL and save a source file. The browser should hot-reload. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index cd413ece4c..6b242e0821 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -101,7 +101,7 @@ Then start the dev server: npm run dev ``` -Open the URL the CLI prints, `http://veryfront.me:3000` by default, to see the +Open the URL the CLI prints, `http://localhost:3000` by default, to see the page. For where the remaining files go, see @@ -202,7 +202,7 @@ veryfront install --target agents Then run `veryfront dev`. It starts an HTTP MCP server two ports above the port the dev server actually bound, always at the path `/mcp`. With the default port -3000 that is `http://veryfront.me:3002/mcp`, so point your MCP-aware coding +3000 that is `http://localhost:3002/mcp`, so point your MCP-aware coding agent there. Read the port off the URL the dev server printed rather than off the `--port` diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 754694cbf6..3ca17f897b 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -81,11 +81,11 @@ The CLI prints the URL it is serving on: ``` ✓ Ready in 1.3s - http://veryfront.me:3000 + http://localhost:3000 ``` -`veryfront.me` resolves to `127.0.0.1`, so -[http://localhost:3000](http://localhost:3000) reaches the same server. +`localhost` resolves to `127.0.0.1` on every machine without a DNS lookup, so +[http://localhost:3000](http://localhost:3000) always reaches the dev server. The dev server uses port 3000 by default. You can also set the `PORT` env var instead of the flag; `veryfront dev` reads it as a lower-precedence default, @@ -109,7 +109,7 @@ Use [Coding agents](../guides/coding-agents.md) for setup details. ## Verify it worked Open the URL `veryfront dev` printed. Unless the port moved, that is -[http://veryfront.me:3000](http://veryfront.me:3000). Ask: +[http://localhost:3000](http://localhost:3000). Ask: ```text What is 128 divided by 8? diff --git a/docs/guides/coding-agents.md b/docs/guides/coding-agents.md index cb476155f5..9f90865f4d 100644 --- a/docs/guides/coding-agents.md +++ b/docs/guides/coding-agents.md @@ -112,7 +112,7 @@ port the dev server bound (default `3002`). The endpoint is always `/mcp`. http://localhost:3002/mcp # dev ``` -The dev server also accepts the `veryfront.me` hostname, which resolves to `127.0.0.1` and is what the CLI prints by default. +`localhost` is what the CLI prints by default; `127.0.0.1` reaches the same server. ## Connect Claude Code @@ -122,7 +122,7 @@ Add an `mcpServers` entry in `~/.claude.json`: { "mcpServers": { "veryfront": { - "url": "http://veryfront.me:3002/mcp" + "url": "http://localhost:3002/mcp" } } } @@ -213,4 +213,4 @@ The dev MCP port is always two above the port the dev server bound, so a dev ser ### CORS error from a browser-based agent -The HTTP MCP only accepts requests from `localhost`, `127.0.0.1`, and `veryfront.me`. Browser agents that run from any other origin are rejected by design. +The HTTP MCP only accepts requests from `localhost`, `127.0.0.1`, and `[::1]`. Browser agents that run from any other origin are rejected by design. diff --git a/scripts/docs/validate-public-docs.ts b/scripts/docs/validate-public-docs.ts index 04c93b2056..875654ab0d 100644 --- a/scripts/docs/validate-public-docs.ts +++ b/scripts/docs/validate-public-docs.ts @@ -193,11 +193,11 @@ const DEPLOY_ACCESS_COVERAGE: CoveragePage[] = [ ]; /** - * `veryfront dev` prints `http://veryfront.me:` and no other URL. Pages + * `veryfront dev` prints `http://localhost:` and no other URL. Pages * that run the dev server and then tell the reader to open the app must name * that host, or the reader hits a banner that matches nothing in the doc. */ -const PRINTED_DEV_SERVER_URL = "http://veryfront.me:3000"; +const PRINTED_DEV_SERVER_URL = "http://localhost:3000"; const DEV_SERVER_PAGES = [ "docs/getting-started/quickstart.md", "docs/getting-started/create-project.md", diff --git a/src/agent/service/config.test.ts b/src/agent/service/config.test.ts index ab8c22d9dc..375ed8cd02 100644 --- a/src/agent/service/config.test.ts +++ b/src/agent/service/config.test.ts @@ -55,7 +55,7 @@ describe("agent/agent-service-config", () => { assertEquals(config.VERYFRONT_STUDIO_MCP_URL, ""); assertEquals(config.NODE_ENV, "development"); assertEquals(config.PORT, 3001); - assertEquals(config.ALLOWED_ORIGINS, ["http://localhost:3000", "http://veryfront.me:3000"]); + assertEquals(config.ALLOWED_ORIGINS, ["http://localhost:3000"]); assertEquals(config.OTEL_ENABLED, false); assertEquals(config.VERYFRONT_API_TOKEN, undefined); assertEquals(config.VERYFRONT_PROJECT_ID, undefined); diff --git a/src/agent/service/config.ts b/src/agent/service/config.ts index b530dc2cf7..038b08065d 100644 --- a/src/agent/service/config.ts +++ b/src/agent/service/config.ts @@ -101,7 +101,9 @@ const getAgentServiceConfigSchema = defineSchema((v) => { 64_000, ), VERYFRONT_CONTEXT_COMPACTION_SUMMARY_MODEL: v.string().min(1).optional(), - ALLOWED_ORIGINS: v.string().default("http://localhost:3000,http://veryfront.me:3000"), + // One default entry: the dev server has a single printed origin, and the + // former second entry was an alias of this same loopback origin. + ALLOWED_ORIGINS: v.string().default("http://localhost:3000"), OTEL_ENABLED: booleanFlagSchema, OTEL_EXPORTER_OTLP_ENDPOINT: v.string().optional(), }).transform((env) => ({ diff --git a/src/proxy/handler.test.ts b/src/proxy/handler.test.ts index edeb8974f0..5afda9242b 100644 --- a/src/proxy/handler.test.ts +++ b/src/proxy/handler.test.ts @@ -1663,7 +1663,7 @@ describe("Proxy Handler", () => { for ( const host of [ "lvh.me", - "veryfront.me", + "localhost", "veryfront.dev", "preview.lvh.me", "staging.lvh.me", diff --git a/src/security/http/local-control-request.test.ts b/src/security/http/local-control-request.test.ts index 78e82cd152..fb8ff0cef5 100644 --- a/src/security/http/local-control-request.test.ts +++ b/src/security/http/local-control-request.test.ts @@ -118,12 +118,10 @@ describe("local control request admission", () => { "http://[::1]:3000/_dev", "http://[::ffff:7f00:1]:3000/_dev", "http://project.localhost:3000/_dev", + "http://project.preview.localhost:3000/_dev", "http://lvh.me:3000/_dev", "http://project.lvh.me:3000/_dev", "http://project.preview.lvh.me:3000/_dev", - "http://veryfront.me:3000/_dev", - "http://project.veryfront.me:3000/_dev", - "http://project.preview.veryfront.me:3000/_dev", ] ) { const parsed = new URL(url); @@ -146,9 +144,19 @@ describe("local control request admission", () => { "http://project.staging.lvh.me:3000/_dev", "http://example.com.prod.lvh.me:3000/_dev", "http://project.unknown.lvh.me:3000/_dev", - "http://production.veryfront.me:3000/_dev", - "http://project.staging.veryfront.me:3000/_dev", - "http://project.unknown.veryfront.me:3000/_dev", + // `localhost` is a single label, so it has no registrable domain to key + // the shape check on. It still gets the same shape check as the + // two-label roots: moving the printed dev hostname onto it must not + // promote production, staging, custom-domain simulation, unknown + // namespaces, or arbitrarily deep names into control authorities. + "http://production.localhost:3000/_dev", + "http://staging.localhost:3000/_dev", + "http://project.production.localhost:3000/_dev", + "http://project.staging.localhost:3000/_dev", + "http://project.unknown.localhost:3000/_dev", + "http://example.com.prod.localhost:3000/_dev", + "http://a.b.c.localhost:3000/_dev", + "http://localhost.attacker.example:3000/_dev", "http://attacker.example:3000/_dev", ] ) { diff --git a/src/security/http/local-control-request.ts b/src/security/http/local-control-request.ts index 22d81ebe3b..d06bf40b60 100644 --- a/src/security/http/local-control-request.ts +++ b/src/security/http/local-control-request.ts @@ -83,17 +83,36 @@ function isCanonicalDnsHostname(hostname: string): boolean { hostname.split(".").every((label) => DNS_LABEL_PATTERN.test(label)); } -function hasTrustedNamedLocalControlShape(hostname: string): boolean { - const labels = hostname.split("."); - const registrableDomain = labels.slice(-2).join("."); - if (registrableDomain !== "lvh.me" && registrableDomain !== "veryfront.me") { - return false; +/** + * Trusted local-control roots, longest-suffix-first. + * + * `localhost` is a single label and therefore has no registrable domain in the + * eTLD+1 sense, so the shape check cannot be expressed as "keep the last two + * labels". Each root is matched as a whole suffix instead and the labels in + * front of it are what the shape rules below constrain. + */ +const TRUSTED_LOCAL_CONTROL_ROOTS = Object.freeze(["localhost", "lvh.me"] as const); + +/** Labels in front of a trusted root, or null when the host is not on one. */ +function localControlSubLabels(hostname: string): string[] | null { + for (const root of TRUSTED_LOCAL_CONTROL_ROOTS) { + if (hostname === root) return []; + const suffix = `.${root}`; + if (hostname.endsWith(suffix)) { + return hostname.slice(0, -suffix.length).split("."); + } } - if (labels.length === 2) return true; - if (labels.length === 3) { - return labels[0] !== "production" && labels[0] !== "staging"; + return null; +} + +function hasTrustedNamedLocalControlShape(hostname: string): boolean { + const subLabels = localControlSubLabels(hostname); + if (subLabels === null) return false; + if (subLabels.length === 0) return true; + if (subLabels.length === 1) { + return subLabels[0] !== "production" && subLabels[0] !== "staging"; } - return labels.length === 4 && labels[1] === "preview"; + return subLabels.length === 2 && subLabels[1] === "preview"; } function hasTrustedFetchSite(request: Request): boolean { @@ -109,15 +128,20 @@ function hasTrustedFetchSite(request: Request): boolean { /** * Dedicated authority allowlist for privileged local controls. * - * `veryfront.me` is product-controlled and is the hostname printed by the - * local CLI. Veryfront admits `lvh.me` because the documented - * local-development workflow reaches projects through it; the hostname alone never grants + * `localhost` is reserved by RFC 6761, never leaves the machine, and is the + * hostname printed by the local CLI. Veryfront admits `lvh.me` because the + * documented local-development workflow reaches projects through it; the hostname alone never grants * access because `isTrustedLocalControlRequest` still requires an * authenticated loopback transport peer and no proxy hop. Other third-party * wildcard DNS and development test domains are not control authorities even - * when normal application routing accepts them. Named domains admit only the + * when normal application routing accepts them. Named roots admit only the * bare host, one project label, or one project below `preview`; production, * staging, custom-domain simulation, and unknown namespaces stay denied. + * + * `*.localhost` gets that same shape check rather than a blanket allow. Trust + * must not widen just because the printed dev hostname became a single-label + * root: `project.production.localhost` and `a.b.c.localhost` are denied exactly + * as `project.production.lvh.me` is. */ export function isTrustedLocalControlHostname(hostname: string): boolean { const address = hostname.startsWith("[") && hostname.endsWith("]") @@ -125,7 +149,6 @@ export function isTrustedLocalControlHostname(hostname: string): boolean { : hostname; if (hostname === "localhost" || isLoopbackAddress(address)) return true; if (!isCanonicalDnsHostname(hostname)) return false; - if (hostname.endsWith(".localhost")) return true; return hasTrustedNamedLocalControlShape(hostname); } diff --git a/src/server/handlers/dev/dashboard/access-policy.test.ts b/src/server/handlers/dev/dashboard/access-policy.test.ts index 1ef635eaed..3d39b26315 100644 --- a/src/server/handlers/dev/dashboard/access-policy.test.ts +++ b/src/server/handlers/dev/dashboard/access-policy.test.ts @@ -34,10 +34,8 @@ describe("dashboard access policy", () => { "http://localhost:8000/_dev", "http://127.0.0.1:8000/_dev/api/stats", "http://[::1]:8000/_dev/ui/index.js", - "http://veryfront.me:8000/_dev", - "http://my-project.veryfront.me:8000/_dev/api/stats", - "http://my-project.preview.veryfront.me:8000/_dev/ui/index.js", "http://my-project.localhost:8000/_dev", + "http://my-project.preview.localhost:8000/_dev/ui/index.js", "http://lvh.me:8000/_dev", "http://my-project.lvh.me:8000/_dev/api/stats", "http://my-project.preview.lvh.me:8000/_dev/ui/index.js", @@ -51,17 +49,19 @@ describe("dashboard access policy", () => { for ( const url of [ "http://0.0.0.0:8000/_dev", - "http://production.veryfront.me:8000/_dev", - "http://staging.veryfront.me:8000/_dev", - "http://my-project.production.veryfront.me:8000/_dev", - "http://my-project.staging.veryfront.me:8000/_dev", - "http://my-project.unknown.veryfront.me:8000/_dev", + "http://production.localhost:8000/_dev", + "http://staging.localhost:8000/_dev", + "http://my-project.production.localhost:8000/_dev", + "http://my-project.staging.localhost:8000/_dev", + "http://my-project.unknown.localhost:8000/_dev", + "http://example.com.prod.localhost:8000/_dev", + "http://a.b.c.localhost:8000/_dev", "http://production.lvh.me:8000/_dev", "http://my-project.production.lvh.me:8000/_dev", "http://my-project.staging.lvh.me:8000/_dev", "http://example.com.prod.lvh.me:8000/_dev", "http://my-project.unknown.lvh.me:8000/_dev", - "http://veryfront.me.attacker.example:8000/_dev", + "http://localhost.attacker.example:8000/_dev", "http://lvh.me.attacker.example:8000/_dev", "http://veryfront.dev:8000/_dev", "http://my-project.veryfront.dev:8000/_dev", diff --git a/src/server/handlers/dev/dashboard/index.test.ts b/src/server/handlers/dev/dashboard/index.test.ts index 619b9ca78c..30114fe3fe 100644 --- a/src/server/handlers/dev/dashboard/index.test.ts +++ b/src/server/handlers/dev/dashboard/index.test.ts @@ -181,7 +181,7 @@ describe("DevDashboardHandler admission", () => { it("issues a headless session without requiring optional Dev UI assets", async () => { const handler = new DevDashboardHandler(); const response = (await handler.handle( - dashboardRequest(`http://veryfront.me:3002${DASHBOARD_SESSION_PATH}`), + dashboardRequest(`http://localhost:3002${DASHBOARD_SESSION_PATH}`), localContext(), )).response!; @@ -198,10 +198,10 @@ describe("DevDashboardHandler admission", () => { const rejectedMethod = (await handler.handle( requestFromPeer( new Request( - `http://veryfront.me:3002${DASHBOARD_SESSION_PATH}`, + `http://localhost:3002${DASHBOARD_SESSION_PATH}`, { method: "POST", - headers: { host: "veryfront.me:3002" }, + headers: { host: "localhost:3002" }, body: new ReadableStream({ cancel() { cancelled = true; diff --git a/src/server/handlers/dev/projects/method-policy.test.ts b/src/server/handlers/dev/projects/method-policy.test.ts index 1fe2cb46b4..537f88f7a5 100644 --- a/src/server/handlers/dev/projects/method-policy.test.ts +++ b/src/server/handlers/dev/projects/method-policy.test.ts @@ -32,7 +32,7 @@ describe("ProjectsHandler method policy", () => { }), duplex: "half", }; - const request = new Request("http://veryfront.me/_projects", init); + const request = new Request("http://localhost/_projects", init); let responseSettled = false; const responsePromise = new ProjectsHandler(PROVIDER).handle(request, projectsContext()); void responsePromise.then(() => { @@ -53,11 +53,11 @@ describe("ProjectsHandler method policy", () => { const handler = new ProjectsHandler(PROVIDER); for (const path of ["/_projects", "/_projects/ui/index.js", "/_projects/api/config"]) { const getResponse = (await handler.handle( - new Request(`http://veryfront.me${path}`), + new Request(`http://localhost${path}`), projectsContext(), )).response!; const headResponse = (await handler.handle( - new Request(`http://veryfront.me${path}`, { method: "HEAD" }), + new Request(`http://localhost${path}`, { method: "HEAD" }), projectsContext(), )).response!; @@ -71,7 +71,7 @@ describe("ProjectsHandler method policy", () => { } const unavailable = (await new ProjectsHandler().handle( - new Request("http://veryfront.me/_projects", { method: "HEAD" }), + new Request("http://localhost/_projects", { method: "HEAD" }), projectsContext(), )).response!; assertEquals(unavailable.status, 503); diff --git a/src/server/handlers/preview/hmr.handler.test.ts b/src/server/handlers/preview/hmr.handler.test.ts index 83c4b96607..17f62b1b5d 100644 --- a/src/server/handlers/preview/hmr.handler.test.ts +++ b/src/server/handlers/preview/hmr.handler.test.ts @@ -266,7 +266,7 @@ describe("server/handlers/preview/hmr.handler", () => { const req = new Request("http://example.com/_ws", { headers: { host: "internal.proxy:3000", - "x-forwarded-host": "preview.veryfront.me:3000", + "x-forwarded-host": "preview.localhost:3000", }, }); const ctx = makeCtx({ diff --git a/src/server/utils/domain-parser.test.ts b/src/server/utils/domain-parser.test.ts index f5de740909..4e13c9c790 100644 --- a/src/server/utils/domain-parser.test.ts +++ b/src/server/utils/domain-parser.test.ts @@ -10,8 +10,8 @@ import { describe("domain-parser", () => { describe("parseProjectDomain", () => { - it("veryfront.me preview", () => { - const result = parseProjectDomain("myproject.preview.veryfront.me:8080"); + it("localhost preview", () => { + const result = parseProjectDomain("myproject.preview.localhost:8080"); assertEquals(result.slug, "myproject"); assertEquals(result.branch, null); assertEquals(result.environment, "preview"); @@ -19,38 +19,38 @@ describe("domain-parser", () => { assertEquals(result.isDraft, true); }); - it("veryfront.me preview with branch", () => { - const result = parseProjectDomain("myproject--feature-x.preview.veryfront.me"); + it("localhost preview with branch", () => { + const result = parseProjectDomain("myproject--feature-x.preview.localhost"); assertEquals(result.slug, "myproject"); assertEquals(result.branch, "feature-x"); assertEquals(result.environment, "preview"); }); - it("veryfront.me base (mirrors production)", () => { - const result = parseProjectDomain("myproject.veryfront.me:8080"); + it("localhost base (mirrors production)", () => { + const result = parseProjectDomain("myproject.localhost:8080"); assertEquals(result.slug, "myproject"); assertEquals(result.environment, "production"); assertEquals(result.isVeryfrontDomain, true); assertEquals(result.isDraft, false); }); - it("veryfront.me prod (custom domain simulation)", () => { - const result = parseProjectDomain("example.com.prod.veryfront.me"); + it("localhost prod (custom domain simulation)", () => { + const result = parseProjectDomain("example.com.prod.localhost"); assertEquals(result.slug, null); assertEquals(result.environment, "production"); assertEquals(result.isVeryfrontDomain, false); }); - it("plain veryfront.me", () => { - const result = parseProjectDomain("veryfront.me"); + it("plain localhost", () => { + const result = parseProjectDomain("localhost"); assertEquals(result.slug, null); assertEquals(result.environment, "development"); assertEquals(result.isVeryfrontDomain, true); assertEquals(result.isDraft, true); }); - it("local preview environment root (veryfront.me)", () => { - const result = parseProjectDomain("preview.veryfront.me"); + it("local preview environment root (localhost)", () => { + const result = parseProjectDomain("preview.localhost"); assertEquals(result.slug, null); assertEquals(result.environment, "preview"); assertEquals(result.isVeryfrontDomain, true); @@ -144,16 +144,16 @@ describe("domain-parser", () => { assertEquals(result.isDraft, false); }); - it("local dev explicit production: {slug}.production.veryfront.me", () => { - const result = parseProjectDomain("myproject.production.veryfront.me:8080"); + it("local dev explicit production: {slug}.production.localhost", () => { + const result = parseProjectDomain("myproject.production.localhost:8080"); assertEquals(result.slug, "myproject"); assertEquals(result.environment, "production"); assertEquals(result.isVeryfrontDomain, true); assertEquals(result.isDraft, false); }); - it("local dev explicit staging: {slug}.staging.veryfront.me", () => { - const result = parseProjectDomain("myproject.staging.veryfront.me:8080"); + it("local dev explicit staging: {slug}.staging.localhost", () => { + const result = parseProjectDomain("myproject.staging.localhost:8080"); assertEquals(result.slug, "myproject"); assertEquals(result.environment, "staging"); assertEquals(result.isVeryfrontDomain, true); @@ -183,16 +183,16 @@ describe("domain-parser", () => { assertEquals(result.isDraft, false); }); - it("local staging environment root (veryfront.me)", () => { - const result = parseProjectDomain("staging.veryfront.me"); + it("local staging environment root (localhost)", () => { + const result = parseProjectDomain("staging.localhost"); assertEquals(result.slug, null); assertEquals(result.environment, "staging"); assertEquals(result.isVeryfrontDomain, true); assertEquals(result.isDraft, false); }); - it("local production environment root (veryfront.me)", () => { - const result = parseProjectDomain("production.veryfront.me"); + it("local production environment root (localhost)", () => { + const result = parseProjectDomain("production.localhost"); assertEquals(result.slug, null); assertEquals(result.environment, "production"); assertEquals(result.isVeryfrontDomain, true); @@ -216,7 +216,7 @@ describe("domain-parser", () => { }); it("local unknown namespace is not recognized", () => { - const result = parseProjectDomain("myproject.foobar.veryfront.me"); + const result = parseProjectDomain("myproject.foobar.localhost"); assertEquals(result.slug, null); assertEquals(result.environment, null); assertEquals(result.isVeryfrontDomain, false); @@ -242,9 +242,9 @@ describe("domain-parser", () => { assertEquals(isVeryfrontDomain("myproject.preview.veryfront.com"), true); }); - it("recognizes veryfront.me", () => { - assertEquals(isVeryfrontDomain("myproject.veryfront.me:8080"), true); - assertEquals(isVeryfrontDomain("veryfront.me"), true); + it("recognizes localhost", () => { + assertEquals(isVeryfrontDomain("myproject.localhost:8080"), true); + assertEquals(isVeryfrontDomain("localhost"), true); }); it("recognizes lvh.me", () => { @@ -327,37 +327,54 @@ describe("domain-parser", () => { }); it("recognizes bare local dev domains", () => { - assertEquals(isLocalDevHost("veryfront.me"), true); + assertEquals(isLocalDevHost("localhost"), true); assertEquals(isLocalDevHost("lvh.me"), true); assertEquals(isLocalDevHost("veryfront.dev"), true); - assertEquals(isLocalDevHost("veryfront.me:8080"), true); + assertEquals(isLocalDevHost("localhost:8080"), true); }); it("recognizes slug-only local dev domains", () => { - assertEquals(isLocalDevHost("myproject.veryfront.me"), true); + assertEquals(isLocalDevHost("myproject.localhost"), true); assertEquals(isLocalDevHost("myproject.lvh.me:3001"), true); assertEquals(isLocalDevHost("myproject.veryfront.dev"), true); }); it("recognizes preview local dev domains", () => { - assertEquals(isLocalDevHost("myproject.preview.veryfront.me"), true); + assertEquals(isLocalDevHost("myproject.preview.localhost"), true); assertEquals(isLocalDevHost("myproject.preview.lvh.me:3001"), true); - assertEquals(isLocalDevHost("preview.veryfront.me"), true); + assertEquals(isLocalDevHost("preview.localhost"), true); assertEquals(isLocalDevHost("preview.lvh.me"), true); }); it("rejects explicit production local dev domains", () => { - assertEquals(isLocalDevHost("myproject.production.veryfront.me"), false); + assertEquals(isLocalDevHost("myproject.production.localhost"), false); assertEquals(isLocalDevHost("myproject.production.lvh.me"), false); }); it("rejects explicit staging local dev domains", () => { - assertEquals(isLocalDevHost("myproject.staging.veryfront.me"), false); + assertEquals(isLocalDevHost("myproject.staging.localhost"), false); assertEquals(isLocalDevHost("myproject.staging.lvh.me"), false); }); it("rejects custom domain simulation", () => { assertEquals(isLocalDevHost("example.com.prod.lvh.me"), false); + assertEquals(isLocalDevHost("example.com.prod.localhost"), false); + }); + + // `localhost` is a single label with no registrable domain, so `*.localhost` + // cannot be admitted by a blanket suffix test the way a two-label root can. + // It goes through the same parse, which keeps the non-dev namespaces out. + it("classifies *.localhost by the same rules as the two-label local roots", () => { + for (const root of ["localhost", "lvh.me"]) { + assertEquals(isLocalDevHost(`myproject.production.${root}:3000`), false, root); + assertEquals(isLocalDevHost(`myproject.staging.${root}:3000`), false, root); + assertEquals(isLocalDevHost(`staging.${root}`), false, root); + assertEquals(isLocalDevHost(`myproject.foobar.${root}`), false, root); + assertEquals(isLocalDevHost(`example.com.prod.${root}`), false, root); + assertEquals(isLocalDevHost(`a.b.c.${root}`), false, root); + assertEquals(isLocalDevHost(`myproject.${root}`), true, root); + assertEquals(isLocalDevHost(`myproject.preview.${root}`), true, root); + } }); it("rejects custom domains", () => { @@ -371,7 +388,7 @@ describe("domain-parser", () => { }); it("rejects unknown namespace on local dev domains", () => { - assertEquals(isLocalDevHost("myproject.foobar.veryfront.me"), false); + assertEquals(isLocalDevHost("myproject.foobar.localhost"), false); }); }); @@ -380,7 +397,7 @@ describe("domain-parser", () => { assertEquals(parseProjectDomain("myproject.production.veryfront.com").allowIframeEmbed, true); assertEquals(parseProjectDomain("myproject.preview.veryfront.com").allowIframeEmbed, true); assertEquals(parseProjectDomain("myproject.lvh.me").allowIframeEmbed, true); - assertEquals(parseProjectDomain("myproject.veryfront.me").allowIframeEmbed, true); + assertEquals(parseProjectDomain("myproject.localhost").allowIframeEmbed, true); }); it("allows embed for localhost", () => { diff --git a/src/server/utils/domain-parser.ts b/src/server/utils/domain-parser.ts index 8bbd8a1e7a..03a2714049 100644 --- a/src/server/utils/domain-parser.ts +++ b/src/server/utils/domain-parser.ts @@ -10,10 +10,13 @@ export interface ParsedDomain { type Environment = ParsedDomain["environment"]; -// Local development domains (veryfront.me preferred, lvh.me alternative, veryfront.dev for HTTPS testing) -// `localhost` included so that {slug}.localhost URLs work — *.localhost is a W3C Secure Context, -// enabling navigator.mediaDevices / getUserMedia in WKWebView (Tauri) and all browsers. -const LOCAL_DEV_DOMAINS = "veryfront\\.me|lvh\\.me|veryfront\\.dev|localhost"; +// Local development domains (localhost preferred, lvh.me alternative, veryfront.dev for HTTPS testing) +// `localhost` is the hostname the CLI prints: {slug}.localhost URLs work and *.localhost is a +// W3C Secure Context, enabling navigator.mediaDevices / getUserMedia in WKWebView (Tauri) and +// all browsers. Unlike lvh.me and veryfront.dev it is a *single-label* root with no registrable +// domain, so every rule below matches it as a root in its own right rather than via an eTLD+1 +// style "last two labels" split. +const LOCAL_DEV_DOMAINS = "localhost|lvh\\.me|veryfront\\.dev"; // Production domains const PROD_DOMAINS = "veryfront\\.com|veryfront\\.org"; @@ -70,13 +73,16 @@ function matchDomain(domain: string, pattern: string): RegExpMatchArray | null { export function parseProjectDomain(host: string): ParsedDomain { const domain = stripPort(host); - if (IFRAME_EMBED_DOMAINS.test(domain)) { - return createParsedDomain(null, null, "development", false, true, true); + // Plain local dev domains without slug. + // Bare `localhost` is checked here, ahead of IFRAME_EMBED_DOMAINS, so that it is a full + // veryfront local-dev root like bare lvh.me: a project-less local host is how the project + // chooser is reached, and it must not fall through to the custom-domain lookup path. + if (domain === "localhost" || domain === "veryfront.dev" || domain === "lvh.me") { + return createParsedDomain(null, null, "development", true, true); } - // Plain local dev domains without slug - if (domain === "veryfront.me" || domain === "veryfront.dev" || domain === "lvh.me") { - return createParsedDomain(null, null, "development", true, true); + if (IFRAME_EMBED_DOMAINS.test(domain)) { + return createParsedDomain(null, null, "development", false, true, true); } // Local development preview: {slug}.preview.{lvh.me|veryfront.dev} @@ -191,12 +197,12 @@ export function isHostedVeryfrontDomain(host: string): boolean { } /** - * Check if a domain is a valid veryfront domain (includes veryfront.me and lvh.me for local dev) + * Check if a domain is a valid veryfront domain (includes localhost and lvh.me for local dev) */ export function isVeryfrontDomain(host: string): boolean { const domain = stripPort(host); - if (domain === "veryfront.me" || domain === "veryfront.dev" || domain === "lvh.me") return true; + if (domain === "localhost" || domain === "veryfront.dev" || domain === "lvh.me") return true; return new RegExp(`^[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\\.(${ALL_DOMAINS})$`).test(domain); } @@ -204,21 +210,26 @@ export function isVeryfrontDomain(host: string): boolean { /** * Check if a host is a local development host where HMR connections should be allowed. * Recognises localhost, 127.0.0.1, 0.0.0.0, *.localhost, and local dev domains - * (veryfront.me, lvh.me, veryfront.dev) — but excludes explicit production/staging + * (lvh.me, veryfront.dev) — but excludes explicit production/staging * subdomains ({slug}.production.{local}, {slug}.staging.{local}) since those are * used for testing non-dev behaviour locally. + * + * `*.localhost` is deliberately NOT a blanket allow. It is classified by the same + * `parseProjectDomain` rules as the two-label local roots, so + * `{slug}.production.localhost` and unknown namespaces such as `{slug}.foobar.localhost` + * stay excluded exactly as their lvh.me counterparts do. A single-label root would + * otherwise widen HMR admission the moment the printed dev hostname moved to localhost. */ export function isLocalDevHost(host: string): boolean { const domain = stripPort(host).toLowerCase(); // Standard loopback / bind-all addresses if (domain === "localhost" || domain === "127.0.0.1" || domain === "0.0.0.0") return true; - // W3C *.localhost secure context - if (domain.endsWith(".localhost")) return true; - // Must be on a local dev TLD — production domains (veryfront.com/org) are not dev hosts - const isLocalTLD = /\.(veryfront\.me|lvh\.me|veryfront\.dev)$/i.test(domain) || - /^(veryfront\.me|lvh\.me|veryfront\.dev)$/i.test(domain); + // Must be on a local dev root — production domains (veryfront.com/org) are not dev hosts. + // `localhost` is a single-label root; the others are two-label registrable domains. + const isLocalTLD = /\.(localhost|lvh\.me|veryfront\.dev)$/i.test(domain) || + /^(lvh\.me|veryfront\.dev)$/i.test(domain); if (!isLocalTLD) return false; const parsed = parseProjectDomain(host); diff --git a/src/server/utils/request-host.test.ts b/src/server/utils/request-host.test.ts index 668b5d6507..7b487e5d2a 100644 --- a/src/server/utils/request-host.test.ts +++ b/src/server/utils/request-host.test.ts @@ -13,15 +13,15 @@ describe("server/utils/request-host", () => { it("returns the first forwarded host entry", () => { assertEquals( - parseForwardedHost("preview.veryfront.me:3000, proxy.internal"), - "preview.veryfront.me:3000", + parseForwardedHost("preview.localhost:3000, proxy.internal"), + "preview.localhost:3000", ); }); it("trims surrounding whitespace from the selected entry", () => { assertEquals( - parseForwardedHost(" preview.veryfront.me:3000 , proxy.internal"), - "preview.veryfront.me:3000", + parseForwardedHost(" preview.localhost:3000 , proxy.internal"), + "preview.localhost:3000", ); }); }); @@ -30,7 +30,7 @@ describe("server/utils/request-host", () => { it("ignores x-forwarded-host by default (untrusted) and uses the host header", () => { const req = new Request("http://127.0.0.1:3000/test", { headers: { - "x-forwarded-host": "preview.veryfront.me:3000, proxy.internal", + "x-forwarded-host": "preview.localhost:3000, proxy.internal", "host": "localhost:3000", }, }); @@ -43,14 +43,14 @@ describe("server/utils/request-host", () => { it("prefers x-forwarded-host over host and url host when proxy is trusted", () => { const req = new Request("http://127.0.0.1:3000/test", { headers: { - "x-forwarded-host": "preview.veryfront.me:3000, proxy.internal", + "x-forwarded-host": "preview.localhost:3000, proxy.internal", "host": "localhost:3000", }, }); assertEquals( getEffectiveRequestHost(req, undefined, true), - "preview.veryfront.me:3000", + "preview.localhost:3000", ); }); @@ -63,9 +63,9 @@ describe("server/utils/request-host", () => { }); it("falls back to request url host when no forwarded or host headers exist", () => { - const req = new Request("http://preview.veryfront.me:3000/test"); + const req = new Request("http://preview.localhost:3000/test"); - assertEquals(getEffectiveRequestHost(req), "preview.veryfront.me:3000"); + assertEquals(getEffectiveRequestHost(req), "preview.localhost:3000"); }); }); }); diff --git a/tests/integration/server/modules/hmr-handler.test.ts b/tests/integration/server/modules/hmr-handler.test.ts index 772fc56f43..17f9fb3dc4 100644 --- a/tests/integration/server/modules/hmr-handler.test.ts +++ b/tests/integration/server/modules/hmr-handler.test.ts @@ -182,11 +182,11 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, assertEquals(result.response, undefined); }); - it("does not treat *.production.veryfront.me as localhost", async () => { + it("does not treat *.production.localhost as localhost", async () => { const handler = new HMRHandler(); const req = new Request("http://localhost:3000/_ws", { - headers: { host: "myproject.production.veryfront.me:3000" }, + headers: { host: "myproject.production.localhost:3000" }, }); const ctx = { requestContext: { mode: "production" }, @@ -202,11 +202,11 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, assertEquals(result.response, undefined); }); - it("does not treat *.staging.veryfront.me as localhost", async () => { + it("does not treat *.staging.localhost as localhost", async () => { const handler = new HMRHandler(); const req = new Request("http://localhost:3000/_ws", { - headers: { host: "myproject.staging.veryfront.me:3000" }, + headers: { host: "myproject.staging.localhost:3000" }, }); const ctx = { requestContext: { mode: "production" }, @@ -222,11 +222,11 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, assertEquals(result.response, undefined); }); - it("does not treat unknown *.veryfront.me namespace as localhost", async () => { + it("does not treat unknown *.localhost namespace as localhost", async () => { const handler = new HMRHandler(); const req = new Request("http://localhost:3000/_ws", { - headers: { host: "myproject.foobar.veryfront.me:3000" }, + headers: { host: "myproject.foobar.localhost:3000" }, }); const ctx = { requestContext: { mode: "production" }, @@ -246,7 +246,7 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, const handler = new HMRHandler(); const req = new Request("http://localhost:3000/_ws", { - headers: { host: "preview.veryfront.me:3000" }, + headers: { host: "preview.localhost:3000" }, }); const ctx = { requestContext: { mode: "production" }, @@ -264,7 +264,7 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, it("IGNORES x-forwarded-host when the request is NOT proxy-trusted (VULN-SRV-4)", async () => { // Without a trusted-proxy signal the handler MUST NOT honour x-forwarded-host - // — otherwise any remote client could claim `x-forwarded-host: preview.veryfront.me` + // — otherwise any remote client could claim `x-forwarded-host: preview.localhost` // and unlock HMR on a production deployment. The raw Host header ("internal.proxy") // is non-local, so the handler should decline. const handler = new HMRHandler(); @@ -272,7 +272,7 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, const req = new Request("http://internal.proxy:3000/_ws", { headers: { host: "internal.proxy:3000", - "x-forwarded-host": "preview.veryfront.me:3000", + "x-forwarded-host": "preview.localhost:3000", }, }); const ctx = { @@ -298,7 +298,7 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, const req = new Request("http://internal.proxy:3000/_ws", { headers: { host: "internal.proxy:3000", - "x-forwarded-host": "preview.veryfront.me:3000", + "x-forwarded-host": "preview.localhost:3000", "x-veryfront-dispatch-jws": jws, }, }); @@ -328,7 +328,7 @@ describe("HMR Handler Tests", { sanitizeOps: false, sanitizeResources: false }, const req = new Request("http://internal.proxy:3000/_ws", { headers: { host: "internal.proxy:3000", - "x-forwarded-host": "preview.veryfront.me:3000", + "x-forwarded-host": "preview.localhost:3000", "x-veryfront-dispatch-jws": "attacker-supplied.bogus.value", }, }); diff --git a/tests/server/context/request-context.test.ts b/tests/server/context/request-context.test.ts index 8663048538..cdd41faabd 100644 --- a/tests/server/context/request-context.test.ts +++ b/tests/server/context/request-context.test.ts @@ -73,7 +73,7 @@ describe("request-context", () => { it("sets preview mode from local preview environment root domain", () => { const ctx = createRequestContext( - new Request("http://preview.veryfront.me:8080/page"), + new Request("http://preview.localhost:8080/page"), ); assertEquals(ctx.slug, "");